什么是“异步调用”?

“异步调用”对应的是“同步调用”,同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行;异步调用指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序。

我们在项目中经常会用到异步调用,比如在我们没有用到消息系统的情况下,用户注册时,需要发送注册成功的短信或邮件,再页面返回注册成功。发送短信邮件这一步,如果使用同步调用,有时候短信或邮件服务器网络不好,这一步可能会消耗一定的时间,用户需要长时间等待注册结果。而其实只有注册逻辑正确,我们可以认为用户就是注册成功了,可以直接返回结果而不需要等待短信邮件的发送结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Service
@Slf4j
public class UserService {

@Autowired
private UserMapper userMapper;

public RespInfo<String> register(User user) throws Exception {
//添加新用户记录
int count = userMapper.insert(user);
if (count == 1) {
//用户注册成功
//发送用户注册成功短信 不需要等结果 可以改成异步调用
this.sendMessage(user);
//发送用户注册成功邮件 不需要等结果 可以改成异步调用
this.sendEmail(user);
log.info("注册成功 user:{}", user);
return RespInfo.success("注册成功");
}
return RespInfo.error("注册失败");
}

private void sendMessage(User user) throws Exception {
//模拟发送短信耗时
Thread.sleep(500);
log.info("发送短信成功 user:{}", user);
}

private void sendEmail(User user) throws Exception {
//模拟发送邮件耗时
Thread.sleep(1000);
log.info("发送邮件成功 user:{}", user);
}
}

我们用一个测试类调用一下看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RunWith(SpringRunner.class)
@SpringBootTest(classes = DemoApplication.class)
@Slf4j
public class Test1 {
@Autowired
private UserService userService;
@Test
public void testRegister() throws Exception{
User user = new User();
user.setUsername("zhangsan2");
user.setPassword("123456");
userService.register(user);
}
}

看一下console输出

1
2
3
4
5
6
2018-04-13 17:01:57.866  INFO 950 --- [           main] Test1                                    : Started Test1 in 7.393 seconds (JVM running for 8.997)
2018-04-13 17:01:58.039 INFO 950 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2018-04-13 17:01:58.395 INFO 950 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2018-04-13 17:01:58.965 INFO 950 --- [ main] com.tt.study.demo.service.UserService : 发送短信成功 user:User(id=3, username=zhangsan2, password=123456)
2018-04-13 17:01:59.969 INFO 950 --- [ main] com.tt.study.demo.service.UserService : 发送邮件成功 user:User(id=3, username=zhangsan2, password=123456)
2018-04-13 17:01:59.970 INFO 950 --- [ main] com.tt.study.demo.service.UserService : 注册成功 user:User(id=3, username=zhangsan2, password=123456)

可以看出程序按顺序执行了发送短信、发送邮件、返回结果,短信和邮件耗时1.5s。

开启异步调用

在Spring Boot中,我们只需要通过使用@Async注解就能简单的将原来的同步函数变为异步函数(注意函数需要public才能使注解生效,调用类和异步方法不能再同一个类里,否则异步失效),我们新建一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
@Slf4j
public class AsyncTask {
@Async
public void sendMessage(User user) throws Exception {
//模拟发送短信耗时
Thread.sleep(500);
log.info("发送短信成功 user:{}", user);
}

@Async
public void sendEmail(User user) throws Exception {
//模拟发送邮件耗时
Thread.sleep(1000);
log.info("发送邮件成功 user:{}", user);
}
}

修改下原来的调用类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
@Slf4j
public class UserService {

@Autowired
private UserMapper userMapper;
@Autowired
private AsyncTask asyncTask;

public RespInfo<String> register(User user) throws Exception {
//添加新用户记录
int count = userMapper.insert(user);
if (count == 1) {
//用户注册成功
//发送用户注册成功短信 不需要等结果 可以改成异步调用
asyncTask.sendMessage(user);
//发送用户注册成功邮件 不需要等结果 可以改成异步调用
asyncTask.sendEmail(user);
log.info("注册成功 user:{}", user);
return RespInfo.success("注册成功");
}
return RespInfo.error("注册失败");
}
}

为了让@Async注解能够生效,还需要在主程序DemoApplication中配置@EnableAsync,如下所示:

1
2
3
4
5
6
7
8
@SpringBootApplication
...
@EnableAsync
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

运行一下测试类,console输出:

1
2
3
4
2018-04-13 18:06:39.624  INFO 1156 --- [nio-8080-exec-1] .s.a.AnnotationAsyncExecutionInterceptor : No task executor bean found for async processing: no bean of type TaskExecutor and no bean named 'taskExecutor' either
2018-04-13 18:06:39.626 INFO 1156 --- [nio-8080-exec-1] com.tt.study.demo.service.UserService : 注册成功 user:User(id=11, username=zhangsan7, password=123456)
2018-04-13 18:06:40.136 INFO 1156 --- [cTaskExecutor-1] c.tt.study.demo.service.async.AsyncTask : 发送短信成功 user:User(id=11, username=zhangsan7, password=123456)
2018-04-13 18:06:40.635 INFO 1156 --- [cTaskExecutor-2] c.tt.study.demo.service.async.AsyncTask : 发送邮件成功 user:User(id=11, username=zhangsan7, password=123456)

可以看到先返回了注册成功,发送短信和发送邮件在另外两个线程执行并输出。这样用户体验就更好了,程序运行时间也变短了。

使用自定义线程池

我们可以使用自定义线程池来控制异步调用,线程池的作用有:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。

定义线程池

我们新建一个TaskPoolConfig类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@EnableAsync
public class TaskPoolConfig {
@Bean
public Executor taskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程数10:线程池创建时候初始化的线程数
executor.setCorePoolSize(10);
//最大线程数20:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
executor.setMaxPoolSize(20);
//缓冲队列200:用来缓冲执行任务的队列
executor.setQueueCapacity(200);
//允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
executor.setKeepAliveSeconds(60);
//线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
executor.setThreadNamePrefix("taskExecutor-");
//线程池对拒绝任务的处理策略:这里采用了CallerRunsPolicy策略,当线程池没有处理能力的时候,
//该策略会直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}

使用线程池

在定义了线程池之后,我们如何让异步调用的执行任务使用这个线程池中的资源来运行呢?方法非常简单,我们只需要在@Async注解中指定线程池名即可,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
@Slf4j
public class AsyncTask {
@Async("taskExecutor")
public void sendMessage(User user) throws Exception {
//模拟发送短信耗时
Thread.sleep(500);
log.info("发送短信成功 user:{}", user);
}

@Async("taskExecutor")
public void sendEmail(User user) throws Exception {
//模拟发送邮件耗时
Thread.sleep(1000);
log.info("发送邮件成功 user:{}", user);
}
}

测试一下

运行我们之前的测试类,查看console,发现有类似输出:

1
2
3
4
5
2018-04-15 13:17:25.255  INFO 2418 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService  'taskExecutor'
...
2018-04-15 13:17:29.541 INFO 2418 --- [ main] com.tt.study.demo.service.UserService : 注册成功 user:User(id=13, username=zhangsan10, password=123456)
2018-04-15 13:17:30.046 INFO 2418 --- [ taskExecutor-1] c.tt.study.demo.service.async.AsyncTask : 发送短信成功 user:User(id=13, username=zhangsan10, password=123456)
2018-04-15 13:17:30.550 INFO 2418 --- [ taskExecutor-2] c.tt.study.demo.service.async.AsyncTask : 发送邮件成功 user:User(id=13, username=zhangsan10, password=123456)

ok,我们用线程池管理异步调用完成了。

本节源码